package ca.tatham.triplipse;

import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.openrdf.model.Resource;
import org.openrdf.model.URI;
import org.openrdf.model.Value;

import ca.tatham.triplipse.filter.IRdfStatementFilter;

/**
 * <P>
 * RDF Queryable Document that builds an in-memory simple database of the
 * triples.
 * </P>
 * 
 * Also keeps track of prefixes, aka namespaces.
 * 
 * @author BenTatham
 * 
 */
public class RdfDocument implements PrefixProvider {
	private List<RdfStatement> m_triples = new LinkedList<RdfStatement>();
	private Map<Value, List<RdfStatement>> m_triplesBySubject = new HashMap<Value, List<RdfStatement>>();
	private Map<Value, List<RdfStatement>> m_triplesByObject = new HashMap<Value, List<RdfStatement>>();
	private Collection<WeakReference<IRdfDocumentChangeListener>> m_listeners = new ArrayList<WeakReference<IRdfDocumentChangeListener>>();
	private Map<String, String> m_prefixes;

	public synchronized Collection<RdfStatement> getTriples() {
		return m_triples;
	}

	public synchronized Collection<RdfStatement> getTriples(
			IRdfStatementFilter filter) {
		if (filter == null) {
			return m_triples;
		}
		Collection<RdfStatement> result = new ArrayList<RdfStatement>(m_triples
				.size());
		for (RdfStatement rdfStatement : m_triples) {
			if (filter.includeStatement(rdfStatement, this)) {
				result.add(rdfStatement);
			}
		}
		return result;
	}

	public synchronized Collection<RdfStatement> getTriplesBySubject(
			Resource subject) {
		return m_triplesBySubject.get(subject);
	}

	public synchronized Collection<RdfStatement> getTriplesByObject(
			Resource subject) {
		return m_triplesByObject.get(subject);
	}

	public synchronized RdfStatement getTriple(Resource subject, URI predicate,
			Value object) {
		for (RdfStatement rdfStatement : getTriplesBySubject(subject)) {
			if (rdfStatement.getPredicate().equals(predicate)
					&& rdfStatement.getObject().equals(object)) {
				return rdfStatement;
			}
		}
		return null;
	}

	/**
	 * 
	 * @param filter
	 *            a IRdfStatementFilter of triples to include, or null for all
	 * @return a collection of triples in which the subject is never an object
	 *         in another triple
	 */
	public synchronized Set<Resource> getUnreferencedSubjects(
			IRdfStatementFilter filter) {
		Set<Resource> result = new HashSet<Resource>();
		for (RdfStatement rdfStatement : getTriples()) {
			if (filter == null || filter.includeStatement(rdfStatement, this)) {
				Resource subject = rdfStatement.getSubject();
				if (!isReferenced(subject) || isCircularReferenced(subject)) {
					result.add(rdfStatement.getSubject());
				}
			}
		}
		return result;
	}

	private boolean isCircularReferenced(Resource subject) {
		Collection<RdfStatement> rdfStatements = getTriplesBySubject(subject);
		return isSubjectInTree(subject, rdfStatements);
	}

	private boolean isSubjectInTree(Resource subject,
			Collection<RdfStatement> rdfStatements) {
		if (rdfStatements != null) {
			for (RdfStatement rdfStatement : rdfStatements) {
				Value object = rdfStatement.getObject();
				if (object.equals(subject)
						|| (object instanceof Resource && isSubjectInTree(
								subject, getTriplesBySubject((Resource) object)))) {
					return true;
				}
			}
		}
		return false;
	}

	private boolean isReferenced(Resource subject) {
		Collection<RdfStatement> rdfStatements = getTriplesByObject(subject);
		return rdfStatements != null && rdfStatements.size() > 0;
	}

	public void setPrefixes(Map<String, String> prefixes) {
		m_prefixes = prefixes;
	}

	public String[] getPrefix(String resource) {
		if (m_prefixes != null) {
			for (String prefix : m_prefixes.keySet()) {
				String uri = m_prefixes.get(prefix);
				if (resource.startsWith(uri)) {
					return new String[] { prefix, uri };
				}
			}
		}
		return null;
	}

	public Map<String, String> getPrefixes() {
		return m_prefixes;
	}

	public void addPrefix(String prefix, String uri) {
		if (m_prefixes == null) {
			m_prefixes = new HashMap<String, String>();
		}
		m_prefixes.put(prefix, uri);
	}

	public void clearPrefixes() {
		m_prefixes = null;
	}

	public synchronized void addTriple(RdfStatement rdfStatement) {
		internalAddTriple(rdfStatement);
		notifyListeners();
	}

	private void internalAddTriple(RdfStatement rdfStatement) {
		m_triples.add(rdfStatement);
		mapTriple(m_triplesBySubject, rdfStatement.getSubject(), rdfStatement);
		mapTriple(m_triplesByObject, rdfStatement.getObject(), rdfStatement);
	}

	private void mapTriple(Map<Value, List<RdfStatement>> tripleMap, Value key,
			RdfStatement rdfStatement) {
		List<RdfStatement> rdfStatements = tripleMap.get(key);
		if (rdfStatements == null) {
			rdfStatements = new ArrayList<RdfStatement>();
			tripleMap.put(key, rdfStatements);
		}
		rdfStatements.add(rdfStatement);
	}

	public synchronized void setTriples(List<RdfStatement> rdfStatements,
			Map<String, String> prefixes) {
		setTriples(rdfStatements);
		setPrefixes(prefixes);
	}

	public synchronized void setTriples(List<RdfStatement> rdfStatements) {
		m_triples.clear();
		m_triplesBySubject.clear();
		m_triplesByObject.clear();
		for (RdfStatement rdfStatement : rdfStatements) {
			internalAddTriple(rdfStatement);
		}
		notifyListeners();
	}

	public void addRdfListener(IRdfDocumentChangeListener listener) {
		m_listeners
				.add(new WeakReference<IRdfDocumentChangeListener>(listener));
	}

	private void notifyListeners() {
		for (WeakReference<IRdfDocumentChangeListener> listenerRef : m_listeners) {
			listenerRef.get().rdfChanged();
		}
	}

}
